von Jörg Jackisch
Sobald wir mit mehreren Teams arbeiten und mehrere Projekte abwickeln und bereitstellen, müssen wir an die Skalierung von Jenkins denken. Dies lässt sich aber mit einer Standardinstallation von Jenkins nur sehr schwer umsetzen, wenn überhaupt. Denn die einzelnen Build-Prozesse auf dem überforderten Jenkins-Server nehmen immer mehr Zeit in Anspruch, was bei der produktiven Zeit der Entwickler und Tester verloren geht. Bei weiter steigender Last auf dem Server wird dieser außerdem zunehmend instabil und fällt öfter aus. Der Frust sowohl bei den Entwickler- als auch Testteams und Teamleitern bis hin zu den Managern ist dann groß. Dieser Frust durch instabile Infrastrukturkomponenten schlägt sich schließlich auch in der Produktivität und Qualität des gesamten Projekts nieder. Das wollen wir natürlich mit allen Mitteln vermeiden. Also muss Skalierung her.
Jenkins skalieren – wie geht das?
Jenkins lässt sich sowohl horizontal als auch vertikal skalieren. In der vertikalen Skalierung kann man den Server mit mehr Hardwareressourcen ausstatten, damit die Jenkins-Applikation performanter ist. Dazu gibt es mehrere Möglichkeiten. Die einfachste ist, die Anzahl der Prozessoren und den Arbeitsspeicher zu erweitern. Auch die I/O-Performance kann man verbessern, indem man bei den Speichermedien zu SSD wechselt und die unterschiedlichen Komponenten mit Fibre Channel verbindet, wenn das Serversystem es zulässt. Bei der vertikalen Skalierung stößt man aber häufig auf technische Grenzen. Hinzu kommt, dass auch die eingesetzten Werkzeuge und Programme, die innerhalb von Jenkins genutzt werden, Multithreading unterstützen müssen, sonst bringt Skalierbarkeit im vertikalen Sektor nicht den gewünschten Effekt. Außerdem bringt das Aufrüsten der Serverhardware keine Ausfallsicherheit mit.
Lesen Sie auch: Grundkurs Microservices: Warum Frameworks nicht genug sind
Da die vertikale Skalierung an Grenzen stößt und man damit keine nachhaltige und langfristige Steigerung erzielen kann, sollte man rechtzeitig die horizontale Skalierbarkeit prüfen und umsetzen. Die horizontale Skalierung beschreibt dabei das Clustering der Anwendung. Jenkins setzt dabei auf eine Master-Agent-(Slave-)Skalierung. Ziel ist es, die Anwendung auf viele Server zu verteilen. Dabei gibt es zwei Alternativen: Zum einen spricht man von einem High-Performance-Computing-(HPC-)Cluster, das dazu dient, die Rechenkapazität zu erhöhen. Zum anderen gibt es High-Available-(HA-)Cluster, die größeren Wert auf die Ausfallsicherheit des Systems legen. Man unterscheidet grundsätzlich zwischen Hardware- und Software-Clustern. In beiden Kategorien gibt es unterschiedliche Methoden der Umsetzung.
Das Jenkins-Cluster in der Theorie
Bei der Einführung eines Jenkins-Clusters beginnt man mit der Einrichtung und Installation der Stand-alone-Variante von Jenkins. Er bildet die Basis des Clusters, verwaltet die gesamte Build-Umgebung und führt über seinen eigenen Executor die Build-Prozesse aus. Der erste Schritt ist es, den Jenkins-Server vertikal zu skalieren, zusätzlich passt man die JRE an. Damit wird man vorübergehend wieder eine stabile Infrastruktur erreichen.
In der Regel ist es allerdings nicht zu empfehlen, die Stand-alone-Variante in einer großen und produktiven Umgebung einzusetzen. Das sollte nur der erste Schritt bei der Einführung von Jenkins sein, denn hier kommt ein großes Sicherheitsrisiko hinzu. Eine Webapplikation führt die Prozesse mit dem Benutzer aus, mit dem der Jenkins-Server gestartet wurde. Meistens ist das ein Benutzer, der erhöhte Rechte besitzt. Das bedeutet, dass der Benutzer der Webapplikation Zugriff auf alle Ressourcen besitzt. Wenn ein Angreifer also Schadcode einschleusen kann, hat er Zugriff auf private oder geheime Daten.
Um vom einzelnen Server zu einer verteilten Lösung zu kommen, bietet Jenkins die Möglichkeit, mehrere sogenannte Worker-Prozesse auf unterschiedliche Server zu verteilen. Dabei bleibt der erste einzelne Server der Master und die weiteren dienen als sogenannte Agents. Dieses Prinzip nennt man Master/Agent Clustering. Dabei verwaltet der Masterserver die komplette Umgebung. Er verteilt einerseits die Deployment-Jobs, andererseits überwacht er, ob die jeweiligen Agents noch verfügbar sind. Der Master dient lediglich dazu, Informationen zu sammeln, Prozesse zu verwalten und letztendlich als grafische Oberfläche des gesamten Schwarms. Die Agents oder Worker sind Server, die nur vom Master Build-Prozesse entgegennehmen und bearbeiten.
Die Master/Agent-Methode gehört zu den HPC-Cluster-Methoden. Die Verteilung der Build-Jobs wird dabei auf alle vorhandenen Worker-Prozesse ausgelagert. Die Verteilung der Jobs lässt sich allerdings auch so konfigurieren, dass bestimmte Abläufe und Prozesse nur auf bestimmten Agents ausgeführt werden.
Damit der Master mit seinen Agents kommunizieren und nachvollziehen kann, ob sie noch verfügbar sind, kann man drei unterschiedliche bidirektionale Kommunikationsmethoden verwenden: Kommunikation per SSH (Secure Shell), JNLP-TCP (JNLP, Java Native Launch Protocol) oder JNLP-HTTP (Java Webstart). Falls es über keinen der vorhandenen Konnektoren möglich ist, eine Verbindung zwischen den Agents und dem Master aufzubauen, kann man auch ein eigenes Skript entwickeln. Als Programmiersprachen für eigene Konnektoren eignen sich vor allem Groovy oder Grails, doch auch Implementierungen mit der Programmiersprache Python sind gängig.
Bei der Methode mit dem SSH-Konnektor fungiert jeder Agent als SSH-Server und der Jenkins-Master ist der SSH-Client; in der Regel via Port 22 mithilfe eines SSH-Schlüssels, der auf dem Agent erstellt wird. Man kann die Verbindung aber auch ohne Schlüssel aufbauen. Dann wird sich der Server wie bei einer gewöhnlichen SSH-Kommunikation mit einem Benutzernamen und einem Passwort anmelden. Es ist empfehlenswert, den Benutzer zu nehmen, mit dem auch der Jenkins-Master läuft. Agents, die auf Microsoft Windows basieren, lassen sich mithilfe des Programms Cygwin verbinden. Cygwin [1] ist eine Sammlung von Open-Source-Werkzeugen, die unter Windows Funktionalitäten eines Linux-Systems bereitstellen.
Jenkins bietet mit dem JNLP-HTTP- und dem JNLP-TCP-Konnektor zwei Varianten, mit denen man mithilfe der Java-internen Protokolle Master und Agent miteinander kommunizieren lassen kann. Um JNLP zu verwenden, arbeitet Jenkins mit der Java-Web-Start-Technologie. Das ist wahrscheinlich die einfachste Art und Weise, Agent und Master zu verknüpfen, da man auf dem Agent lediglich die Java-Applikation des Masters ausführen muss und so bereits die Verbindung aufgebaut hat.
Der Software Architecture Track auf der JAX 2020
Das Jenkins-Cluster in der Praxis
Nach der ganzen Theorie erstellen wir jetzt ganz praktisch mit Docker einen Jenkins-Schwarm. Bei diesem Beispielszenario verbinden wir auf unterschiedliche Art und Weise vier Agents mit einem Masterserver. Es gibt also insgesamt fünf Server. Der Masterserver basiert auf einem Debian Linux, ebenso wie drei der Agents. Zusätzlich wird ein Server hinzugenommen, auf dem Windows läuft. Auf dem Masterknoten des Jenkins-Schwarms läuft das Jenkins Backend. Eine solch heterogene Verteilung bringt den Vorteil, dass man bestimmte Schritte innerhalb des Deployment-Prozesses auf einem bestimmten Betriebssystem ausführen kann. Als Beispiel dienen hier automatisierte Browsertests: Ich möchte meine Java-EE-Applikation sowohl auf einem Windows-Betriebssystem mit Microsoft Edge als auch auf einem Linux-Betriebssystem mit Mozilla Firefox testen.
Basierend auf einem Debian Linux oder einer ähnlichen Linux-Distribution muss man folgende Befehle ausführen, um eine lauffähige Jenkins-Umgebung zu erstellen:
$> apt-get update && apt-get upgrade
Mit apt-get update wird der Paketmanager APT anhand seiner Konfiguration aktualisiert und mit dem Kommando apt-get upgrade das Betriebssystem. Das sollte man stets vor einer Installation einer neuen Software durchführen. Mit dem Befehl $> apt-get install default-jre wget wird die JRE mit all ihren Abhängigkeiten installiert. Hinzu kommt auch das Programm Wget, mit dem Jenkins zur sources-list-Datei von APT hinzugefügt wird. Zum Hinzufügen der Sources für APT benutzt man die Zeilen aus der Dokumentation von Jenkins [2]:
$> wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | apt-key add - $> sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list' $> apt-get update $> apt-get install jenkins
Letztendlich wird mit dem letzten Kommando der Jenkins-Server installiert. Mit Docker kann man diese Installation abkürzen und virtualisiert den Server innerhalb eines Containers. Das Image, das dabei benutzt wird, befindet sich online im Docker Hub. Mit einem Befehl wird ein Container mit Linux erzeugt, auf dem ein Jenkins-Deployment-Server läuft:
$> docker run --name=Master –link=slave3:2222 -d -ti -p 8080:8080 -p 50000:50000 jenkins:latest /bin/bash
Mit den Parametern –p werden die Ports 8080 und 50000 vom Container auf dieselben Ports auf dem Host gemappt. Docker lädt dabei jeden Image-Layer des Docker-Files von Jenkins einzeln herunter und richtet eine lauffähige Umgebung ein. Dabei wird die Bash auf dem Server gestartet, und man kann sich mit dem Befehl docker attach und der Container-ID mit der Bash des Servers verbinden. Zum Starten des Servers führt man den Befehl zum Starten von Jenkins aus: $> java -jar /usr/share/jenkins/jenkins.war &.
Nachdem der Jenkins-Masterserver gestartet ist, kann man in der Docker-Toolbox mit der Tastenkombination STRG + P + Q den Server wieder detachen.
Egal mit welcher Variante der Masterserver eingerichtet wurde, navigiert man im Browser seiner Wahl auf die jeweilige IP-Adresse des Servers. Bei Docker ist das die IP-Adresse des Docker Containers, auf dem der Jenkins-Server läuft, jeweils mit dem Port 8080. Unter http://IP_ADRESSE:8080/ wird jetzt der Set-up-Screen von Jenkins dargestellt. Dort muss man das initiale Passwort eingeben, das bei der Installation angezeigt wurde.
Falls man es übersehen hat, findet man es unter /var/jenkins_home/secrets/initialAdminPassword auf dem Server. Nach dem Set-up kann man sich beim Masterserver anmelden. Zu diesem Zeitpunkt hat man einen Stand-alone-Server eingerichtet, auf dem man die jeweiligen Projekte konfigurieren und einrichten kann.
Zum Erstellen der Slaveserver benutzt man wieder ein Docker Image, das ein Standard-Debian-Betriebssystem bereitstellt. Zum Starten der Server wird folgender Befehl ausgeführt:
docker run -ti --name=Slave1 -d debian:latest /bin/bash docker run -ti --name=Slave2 -d debian:latest /bin/bash docker run -ti --name=Slave3 –hostname=slave3 -p 2222:22 -d debian:latest /bin/bash
Dieser Befehl lädt das aktuelle Debian Image aus dem Docker Hub herunter und startet es als Daemon im Hintergrund (-d = detached). Mit dem Befehl docker ps –format “table {{.ID}}\t{{.Image}}\t{{.Names}}” sollten jetzt alle vier Linux-Server sichtbar sein.
Um die Linux Agents mit dem Masterserver zu verbinden, werden zuerst zwei der Linux-Server per JNLP verbunden. Einer wird per SSH die Verbindung mit dem Masterserver aufbauen. Im Jenkins-Master richtet man für die zwei weiteren Linux-Server jeweils einen weiteren Agent bzw. Knoten ein und lädt dann die Datei slave.jar auf den jeweiligen Slaveserver herunter. Die Knoten sollten sogenannte permanente Knoten sein, und als Startmethode wählt man LAUNCH AGENT VIA JAVA WEB START. Nach der Einrichtung der Agents wird dieser Befehl angezeigt:
Java -jar slave.jar -jnlpURL URL_DES_JENKINS_SERVERS -secret SECRET
Als Parameter für die slave.jar werden die Verbindungsparameter benötigt. Der erste ist –jnlpURL, er teilt der slave.jar mit, wo sich der Server befindet. Der zweite ist ein Secret zur Verbindung (-secret). Den kompletten Befehl, um den Agent zum Master zu verbinden, sieht man, wenn man im Jenkins Backend durch die Punkte JENKINS VERWALTEN | KNOTEN VERWALTEN | SLAVE1 navigiert. Das Gleiche gilt für den zweiten Agent: JENKINS VERWALTEN | KNOTEN VERWALTEN | SLAVE2.
Der dritte Linux-Server kommuniziert per SSH mit dem Masterserver. Dazu geht man wieder mit dem Befehl docker attach in die Bash des Slaves und lädt als Erstes die slave.jar-Datei herunter. Ich benutze dafür Wget und verschiebe die Datei danach in das /bin-Verzeichnis. Zusätzlich muss man hier einen SSH-Server installieren, unter Debian Linux mit dem Befehl apt-get install openssh-server. Im nächsten Schritt legt man einen neuen Benutzer an und gibt ihm ein Passwort.
$> groupadd jenkins $> useradd -g jenkins -d /home/jenkins -s /bin/bash jenkins $> passwd jenkins
Nun erzeugt man einen SSH-Schlüssel, hinterlegt ihn auf dem Jenkins-Server und testet von ihm aus den Log-in.
Im Backend des Masterservers wird auch der dritte Knoten angelegt. Als Startmethode wird hier allerdings LAUNCH AGENT VIA EXECUTION OF COMMAND ON THE MASTER ausgewählt. Nach dieser Auswahl öffnet sich ein neues Konfigurationsfeld, in das man das Startkommando eintragen kann:
ssh -p 2222 [email protected] java -jar /bin/slave.jar
Alternativ bietet Jenkins auch die Startmethode STARTE SLAVE ÜBER SSH. Dort kann man den Hostnamen, Port, Benutzernamen und das Passwort eingeben. Der Jenkins-Server wird sich dann per SSH-Client auf den Server verbinden und ihn als Slave starten. In der Übersicht unter JENKINS VERWALTEN | KNOTEN sollten nun alle Agents dargestellt werden und online sein.
Zur Installation des Windows-Servers empfiehlt es sich, eine virtuelle Maschine zu benutzen. Dann folgt man den Installationsanweisungen des Set-ups von Microsoft. Man sollte allerdings bei der Auswahl der Version darauf achten, was man tatsächlich auf diesem Server ausführen möchte. In diesem Szenario empfiehlt sich Windows in der Clientversion 10, da man dort den Browser Edge zur Verfügung hat. Somit kann man seine Applikation auf Edge testen. Da man keine Serverversion des Betriebssystems benutzt, kann man auch weitere Versionen des Internet Explorers installieren.
Lesen Sie auch: Das Beste aus beiden Welten – Objektfunktionale Programmierung mit Vavr
Unter Windows benutzt man nahezu immer den Verbindungsaufbau per JNLP-HTTP, da dies eine sehr einfache Variante ist, Slaves mit dem Java Web Start zum Jenkins-Masterserver hinzuzufügen. Um den Windows-Server zum Schwarm als Agent hinzuzufügen, verbindet man sich per Remote Desktop zum Windows-Server. Auf diesem benutzt man einen Browser und navigiert zum Jenkins-Master. Nachdem man sich dort eingeloggt hat, klickt man in der Jenkins-Oberfläche auf den Menüpunkt JENKINS VERWALTEN | KNOTEN VERWALTEN und wählt den Menüpunkt NEUER KNOTEN, um einen neuen Agent hinzuzufügen. Im nächsten Fenster füllt man das Textfeld mit dem Namen seines Knotens aus, hier MS Windows Server, und wählt PERMANENT AGENT. Nach der Bestätigung kommt man zur Konfiguration des Knotens.
Das wichtigste Feld bei diesem Formular ist die Startmethode. Dort wählt man in unserem Beispiel LAUNCH AGENT VIA JAVA WEB START. Nach dem Speichern dieses Formulars kommt man zurück zur Übersicht der Knoten. Dort ist nun unser angelegter Windows-Knoten verfügbar, der allerdings mit einem roten Kreuz markiert ist, was bedeutet, dass der Agent nicht verbunden ist. Mit einem Klick auf den Agent kommt man zur Übersichtsseite, wo man mit Klick auf den Java-Webstart-Launch-Knopf die JNLP-Verdingung herstellen kann. Dazu wird eine slave-agent.jnlp-Datei heruntergeladen, die gestartet werden muss, und schon ist der Agent verbunden.
Quarkus-Spickzettel
Quarkus – das Supersonic Subatomic Java Framework. Wollen Sie zeitgemäße Anwendungen mit Quarkus entwickeln? In unserem brandaktuellen Quarkus-Spickzettel finden Sie alles, was Sie zum Loslegen brauchen.
Fazit
Zu Beginn eines Projekts sollte die Planung für den Deployment-Prozess und die Skalierung in der Architekturplanung enthalten sein. Man sieht, wie schnell und einfach man mit Docker einen Jenkins-Schwarm erstellen kann. Läuft der Cluster in einer produktiven Umgebung, kann langfristig und nachhaltig eine qualitativ hochwertige Software garantiert werden.
Links & Literatur
[1] Cygwin: https://www.cygwin.com
[2] Jenkins: https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+Ubuntu
Erfahren Sie mehr über Continuous Delivery auf der JAX 2020:
● Datenbanken in Docker und Kubernetes? Ja, klar!
● Cloud Native Developer Experience: Von Code-Gen bis Git Commit ohne CI/CD Pipeline